package com.leyou.search.service.impl;

import com.leyou.common.dto.PageDTO;
import com.leyou.common.exceptions.LyException;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.*;
import com.leyou.search.dto.SearchParamDTO;
import com.leyou.search.entity.Goods;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import com.leyou.starter.elastic.entity.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.stream.Collectors;

import static com.leyou.search.constants.SearchConstants.*;

/**
 * @author 虎哥
 */
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {

    private final GoodsRepository repository;
    private final ItemClient itemClient;

    public SearchServiceImpl(GoodsRepository repository, ItemClient itemClient) {
        this.repository = repository;
        this.itemClient = itemClient;
    }

    @Override
    public void createIndexAndMapping() {
        // 删除已经存在的索引库
        try {
            repository.deleteIndex();
        } catch (Exception e) {
            log.error("删除失败，可能索引库不存在！", e);
        }
        // 创建索引库
        repository.createIndex("{\n" +
                "  \"settings\": {\n" +
                "    \"analysis\": {\n" +
                "      \"analyzer\": {\n" +
                "        \"my_pinyin\": {\n" +
                "          \"tokenizer\": \"ik_smart\",\n" +
                "          \"filter\": [\n" +
                "            \"py\"\n" +
                "          ]\n" +
                "        }\n" +
                "      },\n" +
                "      \"filter\": {\n" +
                "        \"py\": {\n" +
                "\t\t  \"type\": \"pinyin\",\n" +
                "          \"keep_full_pinyin\": true,\n" +
                "          \"keep_joined_full_pinyin\": true,\n" +
                "          \"keep_original\": true,\n" +
                "          \"limit_first_letter_length\": 16,\n" +
                "          \"remove_duplicated_term\": true\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"mappings\": {\n" +
                "    \"properties\": {\n" +
                "      \"id\": {\n" +
                "        \"type\": \"keyword\"\n" +
                "      },\n" +
                "      \"suggestion\": {\n" +
                "        \"type\": \"completion\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"title\":{\n" +
                "        \"type\": \"text\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"image\":{\n" +
                "        \"type\": \"keyword\",\n" +
                "        \"index\": false\n" +
                "      },\n" +
                "      \"updateTime\":{\n" +
                "        \"type\": \"date\"\n" +
                "      },\n" +
                "      \"specs\":{\n" +
                "        \"type\": \"nested\",\n" +
                "        \"properties\": {\n" +
                "          \"name\":{\"type\": \"keyword\" },\n" +
                "          \"value\":{\"type\": \"keyword\" }\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}");
    }

    @Override
    public void loadData() {
        // 分页信息
        int page = 1, rows = 100;

        while (true) {
            log.info("开始导入第{}页数据", page);
            // 查询商品spu
            PageDTO<SpuDTO> result = itemClient.querySpuByPage(page, rows, true, null, null, null);
            /*List<Goods> goodsList = new ArrayList<>();
            for (SpuDTO spuDTO : result.getItems()) {
                Goods goods = this.buildGoods(spuDTO);
                goodsList.add(goods);
            }*/
            // 把Spu封装为Goods
            List<Goods> goodsList = result.getItems()
                    .stream()
                    .map(this::buildGoods)
                    .collect(Collectors.toList());
            // 批量导入Goods数据到索引库
            repository.saveAll(goodsList);
            log.info("成功导入第{}页数据", page);
            // 翻页
            page++;
            // 判断是否还有
            if (page > result.getTotalPage()) {
                // 没有了
                break;
            }
        }
    }

    @Override
    public Mono<List<String>> getSuggestion(String keyPrefix) {
        if (StringUtils.isBlank(keyPrefix)) {
            throw new LyException(400, "请求参数不能为空！");
        }
        return repository.suggestBySingleField(SUGGESTION_FIELD, keyPrefix);
    }

    @Override
    public Mono<PageInfo<Goods>> search(SearchParamDTO params) {
        // 1.准备查询条件工厂
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 2.source过滤
        sourceBuilder.fetchSource(DEFAULT_SOURCE_FIELD, new String[0]);
        // 3.query条件
        sourceBuilder.query(buildBasicQuery(params));

        // 4.排序条件
        String sortBy = params.getSortBy();
        if (StringUtils.isNotBlank(sortBy)) {
            // 如果有排序字段，我们再完成排序
            sourceBuilder.sort(sortBy, params.getDesc() ? SortOrder.DESC : SortOrder.ASC);
        }

        // 5.分页条件
        sourceBuilder.from(params.getFrom()).size(params.getSize());

        // 6.高亮条件
        sourceBuilder.highlighter(new HighlightBuilder().field(DEFAULT_SEARCH_FIELD)
                .preTags(DEFAULT_PRE_TAG).postTags(DEFAULT_POST_TAG));

        // 7.分页、高亮查询
        return repository.queryBySourceBuilderForPageHighlight(sourceBuilder);
    }

    @Override
    public Mono<Map<String, List<?>>> getFilters(SearchParamDTO params) {
        // 1.查询条件的工厂
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 2.查询条件
        sourceBuilder.query(buildBasicQuery(params));
        sourceBuilder.size(0);
        // 3.聚合条件
        // 3.1.对品牌聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field(BRAND_FIELD_NAME));
        // 3.2.对分类聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("categoryAgg").field(CATEGORY_FIELD_NAME));
        // 3.3.对规格参数聚合
        sourceBuilder.aggregation(
                AggregationBuilders.nested("specAgg", SPEC_FIELD_PATH)
                        .subAggregation(AggregationBuilders.terms("nameAgg").field(SPEC_NAME_FIELD_NAME)
                                .subAggregation(AggregationBuilders.terms("valueAgg").field(SPEC_VALUE_FIELD_NAME))
                        )
        );
        // 4.请求聚合
        Mono<Aggregations> mono = repository.aggregationBySourceBuilder(sourceBuilder);

        // 5.处理结果，转为Map
        return mono.map(aggregations -> {
            // 5.1.创建map，封装最终结果
            Map<String, List<?>> map = new LinkedHashMap<>();
            // 5.2.解析分类结果
            // 5.2.1.根据名称获取聚合结果
            Terms categoryAgg = aggregations.get("categoryAgg");
            // 5.2.2.解析聚合结果，得到分类id
            List<Long> categoryIdList = getIdsFromBucket(categoryAgg);
            // 5.2.3.根据分类id的集合，查询分类集合
            List<CategoryDTO> categories = itemClient.queryCategoryByIds(categoryIdList);
            // 5.2.4.存入map
            map.put("分类", categories);

            // 5.3.解析品牌结果
            // 5.3.1.根据名称获取聚合结果
            Terms brandAgg = aggregations.get("brandAgg");
            // 5.3.2.解析聚合结果，得到分类id
            List<Long> brandIdList = getIdsFromBucket(brandAgg);
            // 5.3.3.根据品牌id的集合，查询品牌集合
            List<BrandDTO> brands = itemClient.queryBrandByIds(brandIdList);
            // 5.3.4.存入map
            map.put("品牌", brands);

            // 5.4.解析规格参数聚合
            // 5.4.1.根据聚合名称获取最外层的nested结果
            Nested specAgg = aggregations.get("specAgg");
            // 5.4.2.获取nameAgg的结果
            Terms nameAgg = specAgg.getAggregations().get("nameAgg");
            // 5.4.3.获取nameAgg的每一个Bucket中的key，作为map中的key
            for (Terms.Bucket nameBucket : nameAgg.getBuckets()) {
                String key = nameBucket.getKeyAsString();
                // 5.4.4.获取valueAgg
                Terms valueAgg = nameBucket.getAggregations().get("valueAgg");
                // 5.4.5.获取value中的Buckets，把Buckets直接转为字符串集合
                List<String> values = valueAgg.getBuckets()
                        .stream()
                        .map(Terms.Bucket::getKeyAsString)
                        .collect(Collectors.toList());
                // 5.4.5.存入map
                map.put(key, values);
            }
            return map;
        });
    }

    @Override
    public void saveGoodsById(Long spuId) {
        // 查询spu
        SpuDTO spu = itemClient.queryGoodsById(spuId);
        // 构建goods
        Goods goods = buildGoods(spu);
        // 新增到索引库
        repository.save(goods);
    }

    @Override
    public void deleteGoodsById(Long spuId) {
        repository.deleteById(spuId);
    }

    private List<Long> getIdsFromBucket(Terms categoryAgg) {
        return categoryAgg.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());
    }

    private QueryBuilder buildBasicQuery(SearchParamDTO params) {
        if (StringUtils.isBlank(params.getKey())) {
            throw new LyException(400, "请求参数不能为空！");
        }
        // 1.创建布尔查询
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        // 2.关键字查询， must
        queryBuilder.must(QueryBuilders.matchQuery(DEFAULT_SEARCH_FIELD, params.getKey()));
        // 3.过滤项查询， filter
        Map<String, String> filters = params.getFilters();
        // 判断是否有过滤项
        if(CollectionUtils.isEmpty(filters)){
            // 没有，直接返回
            return queryBuilder;
        }
        // 4.添加过滤项, {分类: 105, 品牌: 18374, CPU频率: "2.0-2.5GHz"}
        for (Map.Entry<String, String> entry : filters.entrySet()) {
            // 4.1.获取过滤项的key： 分类、品牌、CPU频率
            String key = entry.getKey();
            // 4.2.获取过滤项值：105、18374、2.0-2.5GHz
            String value = entry.getValue();
            // 4.3.添加过滤项
            if("分类".equals(key)){
                // 4.3.1.按照分类来处理
                queryBuilder.filter(QueryBuilders.termQuery(CATEGORY_FIELD_NAME, value));
            }else if("品牌".equals(key)){
                // 4.3.2.按照品牌来处理
                queryBuilder.filter(QueryBuilders.termQuery(BRAND_FIELD_NAME, value));
            } else {
                // 4.3.3.按照规格参数处理，nested类型
                // 1)创建布尔查询，组合name和value查询
                BoolQueryBuilder query = QueryBuilders.boolQuery();
                query.must(QueryBuilders.termQuery(SPEC_NAME_FIELD_NAME, key));
                query.must(QueryBuilders.termQuery(SPEC_VALUE_FIELD_NAME, value));
                // 2)添加到nested查询中
                queryBuilder.filter(QueryBuilders.nestedQuery(SPEC_FIELD_PATH, query, ScoreMode.None));
            }
        }
        return queryBuilder;
    }

    private Goods buildGoods(SpuDTO spu) {
        // 1.自动补全的提示字段
        List<String> suggestion = new ArrayList<>(
                Arrays.asList(StringUtils.split(spu.getCategoryName(), "/")));
        suggestion.add(spu.getName());
        suggestion.add(spu.getBrandName());

        // 2.sku的价格集合
        // 2.1.查询sku集合
        List<SkuDTO> skuList = spu.getSkus();
        if (CollectionUtils.isEmpty(skuList)) {
            // 没有sku，我们去查询
            skuList = itemClient.querySkuBySpuId(spu.getId());
        }
        // 2.2.获取价格集合
        Set<Long> prices = skuList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());

        // 3.商品销量
        long sold = skuList.stream().mapToLong(SkuDTO::getSold).sum();

        // 4.sku的某个图片
        String image = StringUtils.substringBefore(skuList.get(0).getImages(), ",");

        // 5.规格参数
        List<Map<String, Object>> specs = new ArrayList<>();
        // 5.1.查询规格参数name和value键值对，只查询参与搜索的
        List<SpecParamDTO> params = itemClient.querySpecsValues(spu.getId(), true);
        // 5.2.封装
        for (SpecParamDTO param : params) {
            Map<String, Object> map = new HashMap<>(2);
            map.put("name", param.getName());
            map.put("value", chooseSegment(param));
            specs.add(map);
        }

        // 创建Goods对象，并封装数据
        Goods goods = new Goods();
        goods.setUpdateTime(new Date());
        // 自动补全的提示字段
        goods.setSuggestion(suggestion);
        // 规格参数
        goods.setSpecs(specs);
        // 商品销量
        goods.setSold(sold);
        // 商品标题
        goods.setTitle(spu.getTitle() + StringUtils.join(suggestion, " "));
        // sku的价格集合
        goods.setPrices(prices);
        // sku的某个图片
        goods.setImage(image);
        goods.setCategoryId(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setId(spu.getId());
        return goods;
    }

    private Object chooseSegment(SpecParamDTO p) {
        Object value = p.getValue();
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        if (!p.getNumeric() || StringUtils.isBlank(p.getSegments()) || value instanceof Collection) {
            return value;
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }
}
